博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
20行代码实现JavaScript模板引擎
阅读量:6622 次
发布时间:2019-06-25

本文共 6687 字,大约阅读时间需要 22 分钟。

本文首发于个人博客:

正文

刷朋友圈看到了一个不错的题目, 于是Google了一下, 找到一篇文章: , 并不是逐字逐句翻译, 因此算是翻译+笔记吧.

var TemplateEngine = function(tpl, data) {    // magic here ...}var template = '

Hello, my name is <%name%>. I\'m <%age%> years old.

';console.log(TemplateEngine(template, { name: "Krasimir", age: 29}));复制代码

现在我们要实现TemplateEngine函数, 由上可知, 该函数的两个参数为模板及数据. 执行上述代码后会出现以下结果:

Hello, my name is Krasimir. I'm 29 years old.

复制代码

首先我们必须要获取模板中的动态变化部分, 之后将用二个参数中的真实数据替换动态变化部分的内容, 可以使用正则表达式实现.

var re = /<%([^%>]+)?%>/g;复制代码

上面的表达式会提取所有以<%为开头, %>为结尾的部分内容, 末尾的g(global)表示匹配所有项. 然后使用方法, 将所有匹配的字符串存进一个数组中.

var re = /<%([^%>]+)?%>/g;var match = re.exec(tpl);复制代码

输出match得到这样的结果:

[    "<%name%>",    " name ",     index: 21,    input:     "

Hello, my name is <%name%>. I\'m <%age%> years old.

"]复制代码

我们提取出了数据, 但是只得到一个数组元素, 我们需要处理的是所有匹配项, 因此使用while循环实现:

var re = /<%([^%>]+)?%>/g, match;while(match = re.exec(tpl)) {    console.log(match);}复制代码

执行上述代码之后会发现<%name%><%age%>都被提取出来了.

接下来要用真实的数据取代占位符. 最简单的方式是使用String.prototype.replace()方法实现:

var TemplateEngine = function(tpl, data) {    var re = /<%([^%>]+)?%>/g, match;    while(match = re.exec(tpl)) {        tpl = tpl.replace(match[0], data[match[1]])    }    return tpl;}复制代码

对于文章开头的例子, 因为只是简单的对象, 使用当前的方式(data["property"])就能够完成任务, 但是实际上会遇到更复杂的多层嵌套对象, 比如:

{    name: "Krasimir Tsonev",    profile: { age: 29 }}复制代码

将函数的第二个参数改成上述形式之后, 使用以上的方法就没有办法解决问题了, 因为当我们输入<%profile.age%>时, 得到的数据是["profile.age"], 其值为undefined. 此时replace()方法不再适用. 如果对于在<%%>之间的内容, 将其看成JavaScript代码, 可以直接执行并返回值, 那就比较好了, 比如:

var template = '

Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.

';复制代码

使用new Function()语法, 构造函数:

var fn = new Function("arg", "console.log(arg + 1);");fn(2); // outputs 3复制代码

fn函数接受一个参数, 其函数体为console.log(arg + 1), 上述的代码相当于:

var fn = function(arg) {    console.log(arg + 1);}fn(2); // outputs 3复制代码

现在我们知道了可以通过上述方式由字符串构造出一个简单的函数. 不过在实现我们的需求时, 还需要花点时间思考如何构建我们所需的函数体. 该函数的功能是返回编译后的模板. 开始试试看如何实现:

return "

Hello, my name is " + this.name + ". I\'m " + this.profile.age + " years old.

";复制代码

将模板分离为由文本和JavaScript代码组成的部分. 利用简单的合并就可以获得预期的结果. 不过该方法还是无法100%符合我们的要求. 因为如果<%%>之间的内容不是简单的变量, 而是其他更复杂的比如循环语句, 就无法获得预期结果, 例如:

var template = 'My skills:' + '<%for(var index in this.skills) {%>' + '<%this.skills[index]%>' +'<%}%>';复制代码

如果使用简单的合并, 结果是这样的:

return'My skills:' + for(var index in this.skills) { +'' + this.skills[index] +'' +}复制代码

这样的话会产生错误, for(var index in this.skills) {

无法正常执行, 因此采用另一种方式, 不要将所有内容添加到数组中, 而只将所需的内容添加, 最后合并数组:

var r = [];r.push('My skills:'); for(var index in this.skills) {r.push('');r.push(this.skills[index]);r.push('');}return r.join('');复制代码

因此接下来的步骤是在构造的函数体中根据情况添加各行代码, 之前我们已从模板中提取出一些相关的信息: 占位符的内容以及它们所处的位置. 那么, 再定义一个辅助的变量(cursor)就能够实现我们想要得到的结果.

var TemplateEngine = function(tpl, data) {    var re = /<%([^%>]+)?%>/g,        code = 'var r=[];\n',        cursor = 0,         match;    var add = function(line) {        code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';    }    while(match = re.exec(tpl)) {        add(tpl.slice(cursor, match.index));        add(match[1]);        cursor = match.index + match[0].length;    }    add(tpl.substr(cursor, tpl.length - cursor));    code += 'return r.join("");'; // <-- return the result    console.log(code);    return tpl;}var template = '

Hello, my name is <%this.name%>. I\'m <%this.profile.age%> years old.

';console.log(TemplateEngine(template, { name: "Krasimir Tsonev", profile: { age: 29 }}));复制代码

code变量的值为我们自己构造的函数的函数体, 函数体中首先定义了一个空数组. 可以通过cursor变量存储<%this.name%>这种形式的内容之后的文字处于模板中的位置索引值. 然后我们又创建了add函数, 利用这个函数可以添加各行代码到code变量中. 这之后我们会遇到一个棘手的问题, 需要利用转义解决双引号"的问题:

var r=[];r.push("

Hello, my name is ");r.push("this.name");r.push(". I'm ");r.push("this.profile.age");return r.join("");复制代码

this.namethis.profile.age不应该被双引号引起. 可以这样改进add函数来解决这个问题:

var add = function(line, js) {    js? code += 'r.push(' + line + ');\n' :        code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';}var match;while(match = re.exec(tpl)) {    add(tpl.slice(cursor, match.index));    add(match[1], true); // <-- say that this is actually valid js    cursor = match.index + match[0].length;}复制代码

如果占位符的内容为JS代码, 则将其与布尔值true一同传入add函数, 这样就可以得到我们预期的结果:

var r=[];r.push("

Hello, my name is ");r.push(this.name);r.push(". I'm ");r.push(this.profile.age);return r.join("");复制代码

然后我们需要做的就是创建这个函数并执行. 在TemplateEngine函数中不返回tpl, 而是返回我们动态创建的函数:

return new Function(code.replace(/[\r\t\n]/g, '')).apply(data);复制代码

不要在函数中直接传入参数, 利用apply方法调用该函数并传入参数. 这样才会创建正确的作用域, this.name才可正确执行, 此时this指向data对象.

最后我们还想在其中实现一些复杂的操作, 例如if/else声明以及循环:

var template = 'My skills:' + '<%for(var index in this.skills) {%>' + '<%this.skills[index]%>' +'<%}%>';console.log(TemplateEngine(template, {    skills: ["js", "html", "css"]}));复制代码

不过现在会抛出错误Uncaught SyntaxError: Unexpected token for, 通过调试可以发现问题:

var r=[];r.push("My skills:");r.push(for(var index in this.skills) {);r.push("");r.push(this.skills[index]);r.push("");r.push(});r.push("");return r.join("");复制代码

包含for循环的那行代码不应该被添加到数组中, 于是我们这样进行改进:

var re = /<%([^%>]+)?%>/g,    reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g,    code = 'var r=[];\n',    cursor = 0;var add = function(line, js) {    js? code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n' :        code += 'r.push("' + line.replace(/"/g, '\\"') + '");\n';}复制代码

上述代码添加了一个新的正则表达式, 如果JS代码以if, for, else, switch, case, break, { , }这些内容为起始值, 则直接添加该行代码, 不添加到数组中. 那么最后的结果就是:

var r=[];r.push("My skills:");for(var index in this.skills) {r.push("");r.push(this.skills[index]);r.push("");}r.push("");return r.join("");复制代码

这样的话, 所有的内容都被正确编译.

My skills:jshtmlcss复制代码

最后的改进使函数功能更强大, 改进之后我们可以直接在模板里添加复杂逻辑:

var template = 'My skills:' + '<%if(this.showSkills) {%>' +    '<%for(var index in this.skills) {%>' +     '<%this.skills[index]%>' +    '<%}%>' +'<%} else {%>' +    '

none

' +'<%}%>';console.log(TemplateEngine(template, { skills: ["js", "html", "css"], showSkills: true}));复制代码

添加了一些优化项的就类似如下这样:

var TemplateEngine = function(html, options) {    var re = /<%([^%>]+)?%>/g, reExp = /(^( )?(if|for|else|switch|case|break|{|}))(.*)?/g, code = 'var r=[];\n', cursor = 0, match;    var add = function(line, js) {        js? (code += line.match(reExp) ? line + '\n' : 'r.push(' + line + ');\n') :            (code += line != '' ? 'r.push("' + line.replace(/"/g, '\\"') + '");\n' : '');        return add;    }    while(match = re.exec(html)) {        add(html.slice(cursor, match.index))(match[1], true);        cursor = match.index + match[0].length;    }    add(html.substr(cursor, html.length - cursor));    code += 'return r.join("");';    return new Function(code.replace(/[\r\t\n]/g, '')).apply(options);}复制代码

参考

  • (原文)

转载于:https://juejin.im/post/59663eaa6fb9a06ba73d4c35

你可能感兴趣的文章
网络工程师课程---7、网络通信综合实验(做网络基础综合实验 用什么软件)...
查看>>
php实现 简单密码(代码颜色变化)
查看>>
splay树入门(带3个例题)
查看>>
php实现把二叉树打印成多行(谋而后动,写好算法思路,不然浪费超多时间而且还是错误代码,而且精力消耗会导致代码正确率下降以及低级错误)...
查看>>
回溯1--素数环
查看>>
20145328 《信息安全系统设计基础》期中总结
查看>>
20145328 《网络对抗技术》恶意代码分析
查看>>
VirtualBox使用
查看>>
用 Flask 来写个轻博客 (11) — M(V)C_创建视图函数
查看>>
Linux操作常识
查看>>
(5/24) 模块化:实现快速CSS文件打包
查看>>
(14/24) css进阶:(入门)去除冗余的css
查看>>
centos7系统修改网卡名称
查看>>
3T - A1 = ?
查看>>
git init 后关联github仓库是发生错误:
查看>>
SQL表函数的BUG
查看>>
JavaWeb中的文件上传和下载功能的实现
查看>>
linq/EF/lambda Group by/Order by 多个字段详细用法
查看>>
CSS3圆角
查看>>
TCP的状态(SYN,FIN等)
查看>>